'use strict';

const _ = require('lodash');
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const chokidar = require('chokidar');

const notifyPriority = {
  error: 'high',
  copy: 'normal',
  remove: 'normal',
  watch: 'normal',
  'max-depth': 'low',
  'no-delete': 'low'
};

module.exports = function (source, target) {
  syncFiles(source, target, {}, function (event, data) {
    switch (event) {
      case 'error':
        console.error(chalk.bold.red(data.message || data));
        process.exit(data.code || 2);
        break;

      case 'copy':
        console.log(
          '%s %s to %s',
          chalk.bold('COPY'),
          chalk.yellow(path.resolve(data[0])),
          chalk.yellow(path.resolve(data[1]))
        );
        break;

      case 'remove':
        console.log('%s %s', chalk.bold('DELETE'), chalk.yellow(data));
        break;

      case 'watch':
        console.log('%s %s', chalk.bold('WATCHING'), chalk.yellow(data));
        break;

      case 'max-depth':
        console.log('%s: %s too deep', chalk.bold.dim('MAX-DEPTH'), chalk.yellow(data));
        break;

      case 'no-delete':
        // console.log('keep: %s', chalk.yellow(data));
        break;

      // Fallback: forgotten logs, displayed only in verbose mode
      default:
        if (argv.verbose) {
          console.log(event, data);
        }
    }
  });
};

function syncFiles(source, target, opts, notify) {
  opts = _.defaults(opts || {}, {
    watch: false,
    delete: false,
    depth: Infinity,
  });

  if (typeof opts.depth !== 'number' || isNaN(opts.depth)) {
    notify('error', "Expected valid number for option 'depth'");
    return false;
  }

  // Initial mirror
  var mirrored = mirror(source, target, opts, notify, 0);

  if (!mirrored) {
    return false;
  }

  if (opts.watch) {
    // Watcher to keep in sync from that
    chokidar
      .watch(source, {
        persistent: true,
        depth: opts.depth,
        ignoreInitial: true,
        // TODO "ignore": opts.ignore
      })
      //.on("raw", console.log.bind(console, "raw"))
      .on('ready', notify.bind(undefined, 'watch', source))
      .on('add', watcherCopy(source, target, opts, notify))
      .on('addDir', watcherCopy(source, target, opts, notify))
      .on('change', watcherCopy(source, target, opts, notify))
      .on('unlink', watcherDestroy(source, target, opts, notify))
      .on('unlinkDir', watcherDestroy(source, target, opts, notify))
      .on('error', watcherError(opts, notify));
  }
}

function watcherCopy(source, target, opts, notify) {
  return function (f, stats) {
    copy(f, path.join(target, path.relative(source, f)), notify);
  };
}

function watcherDestroy(source, target, opts, notify) {
  return function (f) {
    deleteExtra(path.join(target, path.relative(source, f)), opts, notify);
  };
}

function watcherError(opts, notify) {
  return function (err) {
    notify('error', err);
  };
}

function mirror(source, target, opts, notify, depth) {
  // Specifc case where the very source is gone
  var sourceStat;
  try {
    sourceStat = fs.statSync(source);
  } catch (e) {
    // Source not found: destroy target?
    if (fs.existsSync(target)) {
      return deleteExtra(target, opts, notify);
    }
  }

  var targetStat;
  try {
    targetStat = fs.statSync(target);
  } catch (e) {
    // Target not found? good, direct copy
    return copy(source, target, notify);
  }

  if (sourceStat.isDirectory() && targetStat.isDirectory()) {
    if (depth === opts.depth) {
      notify('max-depth', source);
      return true;
    }

    // copy from source to target
    var copied = fs.readdirSync(source).every(function (f) {
      return mirror(path.join(source, f), path.join(target, f), opts, notify, depth + 1);
    });

    // check for extraneous
    var deletedExtra = fs.readdirSync(target).every(function (f) {
      return fs.existsSync(path.join(source, f)) || deleteExtra(path.join(target, f), opts, notify);
    });

    return copied && deletedExtra;
  } else if (sourceStat.isFile() && targetStat.isFile()) {
    // compare update-time before overwriting
    if (sourceStat.mtime > targetStat.mtime) {
      return copy(source, target, notify);
    } else {
      return true;
    }
  } else if (opts.delete) {
    // incompatible types: destroy target and copy
    return destroy(target, notify) && copy(source, target, notify);
  } else if (sourceStat.isFile() && targetStat.isDirectory()) {
    // incompatible types
    notify('error', "Cannot copy file '" + source + "' to '" + target + "' as existing folder");
    return false;
  } else if (sourceStat.isDirectory() && targetStat.isFile()) {
    // incompatible types
    notify('error', "Cannot copy folder '" + source + "' to '" + target + "' as existing file");
    return false;
  } else {
    throw new Error('Unexpected case: WTF?');
  }
}

function deleteExtra(fileordir, opts, notify) {
  if (opts.delete) {
    return destroy(fileordir, notify);
  } else {
    notify('no-delete', fileordir);
    return true;
  }
}

function copy(source, target, notify) {
  notify('copy', [source, target]);
  try {
    fs.copySync(source, target);
    return true;
  } catch (e) {
    notify('error', e);
    return false;
  }
}

function destroy(fileordir, notify) {
  notify('remove', fileordir);
  try {
    fs.remove(fileordir);
    return true;
  } catch (e) {
    notify('error', e);
    return false;
  }
}
